# 使用 superagent 与 cheerio 完成简单爬虫
30 秒速记
- 分工:
superagent负责把网页HTML拿回来,cheerio负责在服务端用jQuery写法从HTML里挑数据 cheerio只解析字符串,不发请求、不执行页面里的JS,靠前端渲染出来的内容它看不到- 「请求成功」不等于「抓到了」:选择器一个都没命中时返回空数组,要当失败处理
- 请求要设超时、带
User-Agent,控制频率,遵守robots.txt和站点条款
一个简单爬虫就是两步:用 superagent 发 GET 拿到网页源码,再把源码交给 cheerio.load(),像写 jQuery 一样用选择器把要的字段挑出来。 可以把 cheerio 理解成「没有浏览器的 jQuery」,它只认你给它的那段 HTML 字符串,所以页面里靠 JS 渲染出来的内容它是拿不到的,那种情况得找接口或者上 Puppeteer。我写爬虫最在意的是别出现假成功:页面改版之后选择器不命中,接口照样返回 200 和一个空数组,没人会发现,所以结果为空要打日志或者直接报错。
理解「使用 superagent 与 cheerio 完成简单爬虫」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1cheerio.load(html) 之后选择器明明在浏览器里能选中,代码里却拿到空数组,为什么?
大概率那块内容是前端 JS 渲染的,superagent 拿到的源码里根本没有。在浏览器里「查看网页源代码」(不是 Elements 面板)搜一下就知道。这种页面去抓它背后的数据接口,实在不行再用 Puppeteer。
追问 2上游偶尔返回一个验证页或空页面,接口还是 200 加空数组,怎么让它暴露出来?
把「抓到了数据」算进成功条件:items.length === 0 时记录上游状态码和页面标题,返回错误而不是空数组。命中数量做个监控,突然掉到 0 基本就是改版或者被拦了。
追问 3CI 里这个爬虫测试时好时坏,本地一直正常,你会怎么拆?
把网络和解析拆开测。解析部分存一份真实的 HTML 样本当测试数据,断言能解析出几条、字段齐不齐,这部分完全离线、结果稳定;真实请求只留一个冒烟用例,别让外网抖动决定 CI 是否通过。
追问 4请求一直不返回,进程挂在那里不退出,先加什么?
先加超时:superagent.get(url).timeout({ response: 5000, deadline: 10000 })。默认没有超时,对方不回包你就一直等。然后确认失败回调里有 return next(err) 或者显式退出码,别吞错。
追问 5爬别人网站,有哪些红线?
看 robots.txt 和站点条款,不绕登录、不破验证码,请求加间隔、带可识别的 User-Agent,抓过的页面缓存起来别重复打。能用官方 API 就别爬页面。
# 《使用 superagent 与 cheerio 完成简单爬虫》
# 目标
30 秒速记
- 需求:访问
localhost:3000/,实时抓CNode首页,返回[{ title, href }]的JSON - 链路:
Express路由 →superagent请求首页 →cheerio遍历#topic_list .topic_title→res.send(items) href页面上是相对路径/topic/xxx,要补全成绝对地址:new URL(href, base).href- 上游挂了或选择器没命中,要返回错误,别默默给个空数组
这个练习就是做一个「代理接口」:浏览器请求你的 3000 端口,你去抓 CNode 首页,把帖子标题和链接整理成 JSON 数组还回去。 调用方拿到的是干净的结构化数据,不用自己再去解析 HTML,这其实就是很多数据中转服务的原型。实现上要注意两个细节:页面上的链接是 /topic/xxx 这种相对路径,要用 new URL(href, 'https://cnodejs.org/') 补全;上游请求失败要走 next(err),不能把空数组当成功返回。
建立一个 lesson3 项目,在其中编写代码。
当在浏览器中访问 http://localhost:3000/ 时,输出 CNode(https://cnodejs.org/ ) 社区首页的所有帖子标题和链接,以 json 的形式。
输出示例:
[
{
"title": "【公告】发招聘帖的同学留意一下这里",
"href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12"
},
{
"title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
"href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f"
}
]
面试官追问
追问 1返回的数组里只有 title,没有 href,算完成吗?
不算,约定的结构是每条都有 title 和 href。通常是只取了 .text() 忘了 $el.attr('href'),调用方拿到没链接的标题基本没法用。
追问 2href 有的是 /topic/...,有的已经是完整地址,怎么统一?
直接用 new URL(href, 'https://cnodejs.org/').href,相对路径会被补全,绝对地址原样保留。别用字符串拼,容易拼出双斜杠或者重复域名;老写法 url.resolve 已经被标记为过时了。
追问 3接口偶尔返回空数组,但浏览器打开 CNode 明明有帖子,先查哪?
先打日志看上游返回的状态码和 HTML 长度,确认拿到的是不是正常首页。拿到的是正常首页就去查选择器;如果是限流页或者错误页,问题在请求那一侧。
追问 4每次访问都去抓一遍上游,上游慢或者挂了会怎样?怎么改?
你的接口跟着慢、跟着挂。加一层短缓存,比如结果缓存 60 秒,上游失败时返回上一份缓存并标记过期,既减轻对方压力也提高自己的可用性,代价是数据不再是实时的。
# 挑战
30 秒速记
- 在
title、href之外再返回author - 遍历单位要换成「整条帖子」的容器(
#topic_list .cell),在同一个容器里分别找标题和作者 - 作者在头像链接上:
$cell.find('.user_avatar img').attr('title'),或者从头像href的/user/xxx里截出来 - 别分别选出三组节点再按下标拼,中间多一个置顶或广告就全错位
加 author 字段的关键不是多写一个选择器,而是把遍历单位从「标题链接」换成「一整条帖子」,在同一个容器里把三个字段一起取出来。 如果还是只遍历 .topic_title,作者根本不在这个节点里面;如果另外再选一组作者节点按下标对应,只要列表里混进一条置顶或者没头像的帖子,作者就整体错位了。具体选择器以当时的页面为准,CNode 首页的作者名在头像 img 的 title 属性上。
$('#topic_list .cell').each((i, el) => {
const $cell = $(el)
const $title = $cell.find('.topic_title')
items.push({
title: $title.attr('title'),
href: new URL($title.attr('href'), 'https://cnodejs.org/').href,
author: $cell.find('.user_avatar img').attr('title'),
})
})
访问 http://localhost:3000/ 时,输出包括主题的作者,
示例:
[
{
"title": "【公告】发招聘帖的同学留意一下这里",
"href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
"author": "alsotang"
},
{
"title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
"href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
"author": "otheruser"
}
]
面试官追问
追问 1有人直接写 $element.attr('author'),结果拿到什么?
undefined,.topic_title 这个 a 标签上根本没有 author 属性。而且 res.send 序列化 JSON 时 undefined 的字段会被直接丢掉,前端看到的是这一项压根没有 author。
